package com.h2.ref.server.web.social;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.support.OAuth1ConnectionFactory;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.connect.web.ConnectSupport;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.view.RedirectView;

import com.h2.ref.server.user.IUserService;
import com.h2.ref.server.user.model.User;
import com.h2.ref.server.web.ViewConstants;

@Controller
public class ConnectController {
   
   protected static final Logger LOG =
         LoggerFactory.getLogger(ConnectController.class);
   
   ////

   private static final String DUPLICATE_CONNECTION_ATTRIBUTE =
         "social.addConnection.duplicate";
   
   private static final String PROVIDER_ERROR_ATTRIBUTE = 
         "social.provider.error";
   
   ////
   
   private IUserService _userService;

   private ConnectionFactoryLocator _connectionRegistry;
   
   private ConnectionRepositoryInMem _connectionDao;
   
   private final ConnectSupport _webSupport = new ConnectSupport();
   
   ////
   ////
   
   /**
    * Render the status of connections across all providers to the user as HTML
    * in their web browser.
    */
   @RequestMapping(value = ViewConstants.SETTINGS_CONNECT,
         method=RequestMethod.GET)
   public String connectionStatus(NativeWebRequest request, Model model) {
      setNoCache(request);
      processFlash(request, model);
      User user = getUserService().getActiveUser();
      Map<String, List<Connection<?>>> connections =
            getConnectionDao().findAllConnections(user);
      model.addAttribute("providerIds",
            getConnectionRegistry().registeredProviderIds());
      model.addAttribute("connectionMap", connections);
      return ViewConstants.SETTINGS_CONNECTIONS_VIEW;
   }
   
   /**
    * Render the status of the connections to the service provider to the user
    * as HTML in their web browser.
    */
   @RequestMapping(value = ViewConstants.SETTINGS_CONNECT + "/{providerId}",
         method=RequestMethod.GET)
   public String connectionStatus(@PathVariable String providerId,
         NativeWebRequest request, Model model) {
      setNoCache(request);
      processFlash(request, model);
      User user = getUserService().getActiveUser();
      List<Connection<?>> connections =
            getConnectionDao().findConnections(user, providerId);
      if (connections == null || connections.isEmpty()) {
         return connectView(providerId, "connect"); 
      } else {
         model.addAttribute("connections", connections);
         return connectView(providerId, "connected");         
      }
   }
   


   /**
    * Process a connect form submission by commencing the process of
    * establishing a connection to the provider on behalf of the member. For
    * OAuth1, fetches a new request token from the provider, temporarily stores
    * it in the session, then redirects the member to the provider's site for
    * authorization. For OAuth2, redirects the user to the provider's site for
    * authorization.
    */
   @RequestMapping(value = ViewConstants.SETTINGS_CONNECT + "/{providerId}",
         method = RequestMethod.POST)
   public RedirectView connect(@PathVariable String providerId,
         NativeWebRequest request) {
      ConnectionFactory<?> connectionFactory = getConnectionRegistry()
            .getConnectionFactory(providerId);
      MultiValueMap<String, String> parameters =
            new LinkedMultiValueMap<String, String>();
      preConnect(connectionFactory, parameters, request);
      try {
         return new RedirectView(_webSupport.buildOAuthUrl(connectionFactory,
               request, parameters));
      } catch (Exception e) {
         request.setAttribute(PROVIDER_ERROR_ATTRIBUTE, e,
               RequestAttributes.SCOPE_SESSION);
         return connectionStatusRedirect(providerId);
      }
   }

   /**
    * Process the authorization callback from an OAuth 1 service provider.
    * Called after the user authorizes the connection, generally done by having
    * he or she click "Allow" in their web browser at the provider's site. On
    * authorization verification, connects the user's local account to the
    * account they hold at the service provider Removes the request token from
    * the session since it is no longer valid after the connection is
    * established.
    */
   @RequestMapping(value = ViewConstants.SETTINGS_CONNECT + "/{providerId}",
         method = RequestMethod.GET, params = "oauth_token")
   public RedirectView oauth1Callback(@PathVariable String providerId,
         NativeWebRequest request) {
      try {
         OAuth1ConnectionFactory<?> connectionFactory =
               (OAuth1ConnectionFactory<?>) getConnectionRegistry()
                  .getConnectionFactory(providerId);
         Connection<?> connection = _webSupport.completeConnection(
               connectionFactory, request);
         User user = getUserService().getActiveUser();
         getConnectionDao().addConnection(user, connection);
      } catch (Exception e) {
         request.setAttribute(PROVIDER_ERROR_ATTRIBUTE, e,
               RequestAttributes.SCOPE_SESSION);
         LOG.warn("Exception while handling OAuth1 callback ("
               + e.getMessage() + "). Redirecting to " + providerId
               + " connection status page.");
      }
      return connectionStatusRedirect(providerId);
   }

   /**
    * Process the authorization callback from an OAuth 2 service provider.
    * Called after the user authorizes the connection, generally done by having
    * he or she click "Allow" in their web browser at the provider's site. On
    * authorization verification, connects the user's local account to the
    * account they hold at the service provider.
    */
   @RequestMapping(value = ViewConstants.SETTINGS_CONNECT + "/{providerId}",
         method = RequestMethod.GET, params = "code")
   public RedirectView oauth2Callback(@PathVariable String providerId,
         NativeWebRequest request) {
      try {
         OAuth2ConnectionFactory<?> connectionFactory =
               (OAuth2ConnectionFactory<?>) getConnectionRegistry()
                  .getConnectionFactory(providerId);
         Connection<?> connection = _webSupport.completeConnection(
               connectionFactory, request);
         User user = getUserService().getActiveUser();
         getConnectionDao().addConnection(user, connection);
      } catch (Exception e) {
         request.setAttribute(PROVIDER_ERROR_ATTRIBUTE, e,
               RequestAttributes.SCOPE_SESSION);
         LOG.warn("Exception while handling OAuth2 callback ("
               + e.getMessage() + "). Redirecting to " + providerId
               + " connection status page.");
      }
      return connectionStatusRedirect(providerId);
   }

   /**
    * Remove all provider connections for a user account. The user has decided
    * they no longer wish to use the service provider from this application.
    * Note: requires {@link HiddenHttpMethodFilter} to be registered with the
    * '_method' request parameter set to 'DELETE' to convert web browser POSTs
    * to DELETE requests.
    */
   @RequestMapping(value = ViewConstants.SETTINGS_CONNECT + "/{providerId}" + "/delete",
         method = {RequestMethod.POST,RequestMethod.DELETE})
   public RedirectView removeConnections(@PathVariable String providerId,
         NativeWebRequest request) {
      User user = getUserService().getActiveUser();
      getConnectionDao().removeConnections(user, providerId);
      return connectionStatusRedirect(providerId);
   }
   
   ////
   ////
   ////
   
   protected String connectView(String providerId, String type) {
      return ViewConstants.SETTINGS_CONNECTIONS_VIEW
            + '.' + providerId + '.'  + type;
   }
   
   private void processFlash(WebRequest request, Model model) {
      convertSessionAttributeToModelAttribute(DUPLICATE_CONNECTION_ATTRIBUTE,
            request, model);
      convertSessionAttributeToModelAttribute(PROVIDER_ERROR_ATTRIBUTE,
            request, model);
   }

   private void convertSessionAttributeToModelAttribute(String attributeName,
         WebRequest request, Model model) {
      if (request.getAttribute(attributeName, RequestAttributes.SCOPE_SESSION) != null) {
         model.addAttribute(attributeName, Boolean.TRUE);
         request.removeAttribute(attributeName, RequestAttributes.SCOPE_SESSION);         
      }
   }
   
   private void setNoCache(NativeWebRequest request) {
      HttpServletResponse response = request.getNativeResponse(HttpServletResponse.class);
      if (response != null) {
         response.setHeader("Pragma", "no-cache");
         response.setDateHeader("Expires", 1L);
         response.setHeader("Cache-Control", "no-cache");
         response.addHeader("Cache-Control", "no-store");
      }
   }
   
   ////
   ////
   
   /**
    * Returns a RedirectView with the URL to redirect to after a connection is
    * created or deleted. Defaults to "/connect/{providerId}" relative to
    * DispatcherServlet's path. May be overridden to handle custom redirection
    * needs.
    * 
    * @param providerId the ID of the provider for which a connection was created or
    *           deleted.
    */
   protected RedirectView connectionStatusRedirect(String providerId) {
      String path = ViewConstants.SETTINGS_CONNECT + '/' + providerId + ".html";
      return new RedirectView(path, true);
   }
   
   protected void preConnect(ConnectionFactory<?> connectionFactory,
         MultiValueMap<String, String> parameters, WebRequest request) {
      
   }
   
   protected void postConnect(ConnectionFactory<?> connectionFactory,
         Connection<?> connection, WebRequest request) {
      
   }
   
   ////
   ////

   /**
    * @return the connectionRegistry
    */
   public ConnectionFactoryLocator getConnectionRegistry() {
      return _connectionRegistry;
   }

   /**
    * @param connectionRegistry the connectionRegistry to set
    */
   public void setConnectionRegistry(ConnectionFactoryLocator connectionRegistry) {
      _connectionRegistry = connectionRegistry;
   }

   /**
    * @return the connectionDao
    */
   public ConnectionRepositoryInMem getConnectionDao() {
      return _connectionDao;
   }

   /**
    * @param connectionDao the connectionDao to set
    */
   public void setConnectionDao(ConnectionRepositoryInMem connectionDao) {
      _connectionDao = connectionDao;
   }

   /**
    * @return the userService
    */
   public IUserService getUserService() {
      return _userService;
   }

   /**
    * @param userService the userService to set
    */
   public void setUserService(IUserService userService) {
      _userService = userService;
   }
   
}
